/*
 * Copyright (C) 2016 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.google.android.exoplayer2;

import android.annotation.TargetApi;
import android.graphics.SurfaceTexture;
import android.media.MediaCodec;
import android.media.PlaybackParams;
import android.os.Handler;
import android.os.Looper;
import android.support.annotation.Nullable;
import android.util.Log;
import android.view.Surface;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.TextureView;

import com.google.android.exoplayer2.audio.AudioAttributes;
import com.google.android.exoplayer2.audio.AudioRendererEventListener;
import com.google.android.exoplayer2.decoder.DecoderCounters;
import com.google.android.exoplayer2.metadata.Metadata;
import com.google.android.exoplayer2.metadata.MetadataOutput;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.text.Cue;
import com.google.android.exoplayer2.text.TextOutput;
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
import com.google.android.exoplayer2.trackselection.TrackSelector;
import com.google.android.exoplayer2.util.Clock;
import com.google.android.exoplayer2.util.Util;
import com.google.android.exoplayer2.video.VideoRendererEventListener;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CopyOnWriteArraySet;

/**
 * An {@link ExoPlayer} implementation that uses default {@link Renderer} components. Instances can
 * be obtained from {@link ExoPlayerFactory}.
 */
@TargetApi(16)
public class SimpleExoPlayer implements ExoPlayer, Player.VideoComponent, Player.TextComponent {

    /**
     * @deprecated Use {@link com.google.android.exoplayer2.video.VideoListener}.
     */
    @Deprecated
    public interface VideoListener extends com.google.android.exoplayer2.video.VideoListener {
    }

    private static final String TAG = "SimpleExoPlayer";

    protected final Renderer[] renderers;

    private final ExoPlayer player;
    private final ComponentListener componentListener;
    private final CopyOnWriteArraySet<com.google.android.exoplayer2.video.VideoListener> videoListeners;
    private final CopyOnWriteArraySet<TextOutput> textOutputs;
    private final CopyOnWriteArraySet<MetadataOutput> metadataOutputs;
    private final CopyOnWriteArraySet<VideoRendererEventListener> videoDebugListeners;
    private final CopyOnWriteArraySet<AudioRendererEventListener> audioDebugListeners;

    private Format videoFormat;
    private Format audioFormat;

    private Surface surface;
    private boolean ownsSurface;
    @C.VideoScalingMode
    private int videoScalingMode;
    private SurfaceHolder surfaceHolder;
    private TextureView textureView;
    private DecoderCounters videoDecoderCounters;
    private DecoderCounters audioDecoderCounters;
    private int audioSessionId;
    private AudioAttributes audioAttributes;
    private float audioVolume;

    /**
     * @param renderersFactory A factory for creating {@link Renderer}s to be used by the instance.
     * @param trackSelector    The {@link TrackSelector} that will be used by the instance.
     * @param loadControl      The {@link LoadControl} that will be used by the instance.
     */
    protected SimpleExoPlayer(RenderersFactory renderersFactory, TrackSelector trackSelector, LoadControl loadControl) {
        this(renderersFactory, trackSelector, loadControl, Clock.DEFAULT);
    }

    /**
     * @param renderersFactory A factory for creating {@link Renderer}s to be used by the instance.
     * @param trackSelector    The {@link TrackSelector} that will be used by the instance.
     * @param loadControl      The {@link LoadControl} that will be used by the instance.
     * @param clock            The {@link Clock} that will be used by the instance. Should always be {@link
     *                         Clock#DEFAULT}, unless the player is being used from a test.
     */
    protected SimpleExoPlayer(RenderersFactory renderersFactory, TrackSelector trackSelector, LoadControl loadControl, Clock clock) {
        componentListener = new ComponentListener();
        videoListeners = new CopyOnWriteArraySet<>();
        textOutputs = new CopyOnWriteArraySet<>();
        metadataOutputs = new CopyOnWriteArraySet<>();
        videoDebugListeners = new CopyOnWriteArraySet<>();
        audioDebugListeners = new CopyOnWriteArraySet<>();
        Looper eventLooper = Looper.myLooper() != null ? Looper.myLooper() : Looper.getMainLooper();
        Handler eventHandler = new Handler(eventLooper);
        renderers = renderersFactory.createRenderers(eventHandler, componentListener, componentListener, componentListener, componentListener);

        // Set initial values.
        audioVolume = 1;
        audioSessionId = C.AUDIO_SESSION_ID_UNSET;
        audioAttributes = AudioAttributes.DEFAULT;
        videoScalingMode = C.VIDEO_SCALING_MODE_DEFAULT;

        // Build the player and associated objects.
        player = createExoPlayerImpl(renderers, trackSelector, loadControl, clock);
    }

    @Override
    public VideoComponent getVideoComponent() {
        return this;
    }

    @Override
    public TextComponent getTextComponent() {
        return this;
    }

    /**
     * Sets the video scaling mode.
     * <p>
     * <p>Note that the scaling mode only applies if a {@link MediaCodec}-based video {@link Renderer}
     * is enabled and if the output surface is owned by a {@link android.view.SurfaceView}.
     *
     * @param videoScalingMode The video scaling mode.
     */
    @Override
    public void setVideoScalingMode(@C.VideoScalingMode int videoScalingMode) {
        this.videoScalingMode = videoScalingMode;
        for (Renderer renderer : renderers) {
            if (renderer.getTrackType() == C.TRACK_TYPE_VIDEO) {
                player.createMessage(renderer).setType(C.MSG_SET_SCALING_MODE).setPayload(videoScalingMode).send();
            }
        }
    }

    @Override
    public @C.VideoScalingMode
    int getVideoScalingMode() {
        return videoScalingMode;
    }

    @Override
    public void clearVideoSurface() {
        setVideoSurface(null);
    }

    @Override
    public void setVideoSurface(Surface surface) {
        removeSurfaceCallbacks();
        setVideoSurfaceInternal(surface, false);
    }

    @Override
    public void clearVideoSurface(Surface surface) {
        if (surface != null && surface == this.surface) {
            setVideoSurface(null);
        }
    }

    @Override
    public void setVideoSurfaceHolder(SurfaceHolder surfaceHolder) {
        removeSurfaceCallbacks();
        this.surfaceHolder = surfaceHolder;
        if (surfaceHolder == null) {
            setVideoSurfaceInternal(null, false);
        } else {
            surfaceHolder.addCallback(componentListener);
            Surface surface = surfaceHolder.getSurface();
            setVideoSurfaceInternal(surface != null && surface.isValid() ? surface : null, false);
        }
    }

    @Override
    public void clearVideoSurfaceHolder(SurfaceHolder surfaceHolder) {
        if (surfaceHolder != null && surfaceHolder == this.surfaceHolder) {
            setVideoSurfaceHolder(null);
        }
    }

    @Override
    public void setVideoSurfaceView(SurfaceView surfaceView) {
        setVideoSurfaceHolder(surfaceView == null ? null : surfaceView.getHolder());
    }

    @Override
    public void clearVideoSurfaceView(SurfaceView surfaceView) {
        clearVideoSurfaceHolder(surfaceView == null ? null : surfaceView.getHolder());
    }

    @Override
    public void setVideoTextureView(TextureView textureView) {
        removeSurfaceCallbacks();
        this.textureView = textureView;
        if (textureView == null) {
            setVideoSurfaceInternal(null, true);
        } else {
            if (textureView.getSurfaceTextureListener() != null) {
                Log.w(TAG, "Replacing existing SurfaceTextureListener.");
            }
            textureView.setSurfaceTextureListener(componentListener);
            SurfaceTexture surfaceTexture = textureView.isAvailable() ? textureView.getSurfaceTexture() : null;
            setVideoSurfaceInternal(surfaceTexture == null ? null : new Surface(surfaceTexture), true);
        }
    }

    @Override
    public void clearVideoTextureView(TextureView textureView) {
        if (textureView != null && textureView == this.textureView) {
            setVideoTextureView(null);
        }
    }

    /**
     * Sets the stream type for audio playback, used by the underlying audio track.
     * <p>
     * Setting the stream type during playback may introduce a short gap in audio output as the audio
     * track is recreated. A new audio session id will also be generated.
     * <p>
     * Calling this method overwrites any attributes set previously by calling
     * {@link #setAudioAttributes(AudioAttributes)}.
     *
     * @param streamType The stream type for audio playback.
     * @deprecated Use {@link #setAudioAttributes(AudioAttributes)}.
     */
    @Deprecated
    public void setAudioStreamType(@C.StreamType int streamType) {
        @C.AudioUsage int usage = Util.getAudioUsageForStreamType(streamType);
        @C.AudioContentType int contentType = Util.getAudioContentTypeForStreamType(streamType);
        AudioAttributes audioAttributes = new AudioAttributes.Builder().setUsage(usage).setContentType(contentType).build();
        setAudioAttributes(audioAttributes);
    }

    /**
     * Returns the stream type for audio playback.
     *
     * @deprecated Use {@link #getAudioAttributes()}.
     */
    @Deprecated
    public @C.StreamType
    int getAudioStreamType() {
        return Util.getStreamTypeForAudioUsage(audioAttributes.usage);
    }

    /**
     * Sets the attributes for audio playback, used by the underlying audio track. If not set, the
     * default audio attributes will be used. They are suitable for general media playback.
     * <p>
     * Setting the audio attributes during playback may introduce a short gap in audio output as the
     * audio track is recreated. A new audio session id will also be generated.
     * <p>
     * If tunneling is enabled by the track selector, the specified audio attributes will be ignored,
     * but they will take effect if audio is later played without tunneling.
     * <p>
     * If the device is running a build before platform API version 21, audio attributes cannot be set
     * directly on the underlying audio track. In this case, the usage will be mapped onto an
     * equivalent stream type using {@link Util#getStreamTypeForAudioUsage(int)}.
     *
     * @param audioAttributes The attributes to use for audio playback.
     */
    public void setAudioAttributes(AudioAttributes audioAttributes) {
        this.audioAttributes = audioAttributes;
        for (Renderer renderer : renderers) {
            if (renderer.getTrackType() == C.TRACK_TYPE_AUDIO) {
                player.createMessage(renderer).setType(C.MSG_SET_AUDIO_ATTRIBUTES).setPayload(audioAttributes).send();
            }
        }
    }

    /**
     * Returns the attributes for audio playback.
     */
    public AudioAttributes getAudioAttributes() {
        return audioAttributes;
    }

    /**
     * Sets the audio volume, with 0 being silence and 1 being unity gain.
     *
     * @param audioVolume The audio volume.
     */
    public void setVolume(float audioVolume) {
        this.audioVolume = audioVolume;
        for (Renderer renderer : renderers) {
            if (renderer.getTrackType() == C.TRACK_TYPE_AUDIO) {
                player.createMessage(renderer).setType(C.MSG_SET_VOLUME).setPayload(audioVolume).send();
            }
        }
    }

    /**
     * Returns the audio volume, with 0 being silence and 1 being unity gain.
     */
    public float getVolume() {
        return audioVolume;
    }

    /**
     * Sets the {@link PlaybackParams} governing audio playback.
     *
     * @param params The {@link PlaybackParams}, or null to clear any previously set parameters.
     * @deprecated Use {@link #setPlaybackParameters(PlaybackParameters)}.
     */
    @Deprecated
    @TargetApi(23)
    public void setPlaybackParams(@Nullable PlaybackParams params) {
        PlaybackParameters playbackParameters;
        if (params != null) {
            params.allowDefaults();
            playbackParameters = new PlaybackParameters(params.getSpeed(), params.getPitch());
        } else {
            playbackParameters = null;
        }
        setPlaybackParameters(playbackParameters);
    }

    /**
     * Returns the video format currently being played, or null if no video is being played.
     */
    public Format getVideoFormat() {
        return videoFormat;
    }

    /**
     * Returns the audio format currently being played, or null if no audio is being played.
     */
    public Format getAudioFormat() {
        return audioFormat;
    }

    /**
     * Returns the audio session identifier, or {@link C#AUDIO_SESSION_ID_UNSET} if not set.
     */
    public int getAudioSessionId() {
        return audioSessionId;
    }

    /**
     * Returns {@link DecoderCounters} for video, or null if no video is being played.
     */
    public DecoderCounters getVideoDecoderCounters() {
        return videoDecoderCounters;
    }

    /**
     * Returns {@link DecoderCounters} for audio, or null if no audio is being played.
     */
    public DecoderCounters getAudioDecoderCounters() {
        return audioDecoderCounters;
    }

    @Override
    public void addVideoListener(com.google.android.exoplayer2.video.VideoListener listener) {
        videoListeners.add(listener);
    }

    @Override
    public void removeVideoListener(com.google.android.exoplayer2.video.VideoListener listener) {
        videoListeners.remove(listener);
    }

    /**
     * Sets a listener to receive video events, removing all existing listeners.
     *
     * @param listener The listener.
     * @deprecated Use {@link #addVideoListener(com.google.android.exoplayer2.video.VideoListener)}.
     */
    @Deprecated
    public void setVideoListener(VideoListener listener) {
        videoListeners.clear();
        if (listener != null) {
            addVideoListener(listener);
        }
    }

    /**
     * Equivalent to {@link #removeVideoListener(com.google.android.exoplayer2.video.VideoListener)}.
     *
     * @param listener The listener to clear.
     * @deprecated Use {@link
     * #removeVideoListener(com.google.android.exoplayer2.video.VideoListener)}.
     */
    @Deprecated
    public void clearVideoListener(VideoListener listener) {
        removeVideoListener(listener);
    }

    @Override
    public void addTextOutput(TextOutput listener) {
        textOutputs.add(listener);
    }

    @Override
    public void removeTextOutput(TextOutput listener) {
        textOutputs.remove(listener);
    }

    /**
     * Sets an output to receive text events, removing all existing outputs.
     *
     * @param output The output.
     * @deprecated Use {@link #addTextOutput(TextOutput)}.
     */
    @Deprecated
    public void setTextOutput(TextOutput output) {
        textOutputs.clear();
        if (output != null) {
            addTextOutput(output);
        }
    }

    /**
     * Equivalent to {@link #removeTextOutput(TextOutput)}.
     *
     * @param output The output to clear.
     * @deprecated Use {@link #removeTextOutput(TextOutput)}.
     */
    @Deprecated
    public void clearTextOutput(TextOutput output) {
        removeTextOutput(output);
    }

    public void addMetadataOutput(MetadataOutput listener) {
        metadataOutputs.add(listener);
    }

    public void removeMetadataOutput(MetadataOutput listener) {
        metadataOutputs.remove(listener);
    }

    /**
     * Sets an output to receive metadata events, removing all existing outputs.
     *
     * @param output The output.
     * @deprecated Use {@link #addMetadataOutput(MetadataOutput)}.
     */
    @Deprecated
    public void setMetadataOutput(MetadataOutput output) {
        metadataOutputs.clear();
        if (output != null) {
            addMetadataOutput(output);
        }
    }

    /**
     * Equivalent to {@link #removeMetadataOutput(MetadataOutput)}.
     *
     * @param output The output to clear.
     * @deprecated Use {@link #removeMetadataOutput(MetadataOutput)}.
     */
    @Deprecated
    public void clearMetadataOutput(MetadataOutput output) {
        removeMetadataOutput(output);
    }

    /**
     * Sets a listener to receive debug events from the video renderer.
     *
     * @param listener The listener.
     * @deprecated Use {@link #addVideoDebugListener(VideoRendererEventListener)}.
     */
    @Deprecated
    public void setVideoDebugListener(VideoRendererEventListener listener) {
        videoDebugListeners.clear();
        if (listener != null) {
            addVideoDebugListener(listener);
        }
    }

    /**
     * Adds a listener to receive debug events from the video renderer.
     *
     * @param listener The listener.
     */
    public void addVideoDebugListener(VideoRendererEventListener listener) {
        videoDebugListeners.add(listener);
    }

    /**
     * Removes a listener to receive debug events from the video renderer.
     *
     * @param listener The listener.
     */
    public void removeVideoDebugListener(VideoRendererEventListener listener) {
        videoDebugListeners.remove(listener);
    }

    /**
     * Sets a listener to receive debug events from the audio renderer.
     *
     * @param listener The listener.
     * @deprecated Use {@link #addAudioDebugListener(AudioRendererEventListener)}.
     */
    @Deprecated
    public void setAudioDebugListener(AudioRendererEventListener listener) {
        audioDebugListeners.clear();
        if (listener != null) {
            addAudioDebugListener(listener);
        }
    }

    /**
     * Adds a listener to receive debug events from the audio renderer.
     *
     * @param listener The listener.
     */
    public void addAudioDebugListener(AudioRendererEventListener listener) {
        audioDebugListeners.add(listener);
    }

    /**
     * Removes a listener to receive debug events from the audio renderer.
     *
     * @param listener The listener.
     */
    public void removeAudioDebugListener(AudioRendererEventListener listener) {
        audioDebugListeners.remove(listener);
    }

    // ExoPlayer implementation

    @Override
    public Looper getPlaybackLooper() {
        return player.getPlaybackLooper();
    }

    @Override
    public void addListener(Player.EventListener listener) {
        player.addListener(listener);
    }

    @Override
    public void removeListener(Player.EventListener listener) {
        player.removeListener(listener);
    }

    @Override
    public int getPlaybackState() {
        return player.getPlaybackState();
    }

    @Override
    public void prepare(MediaSource mediaSource) {
        player.prepare(mediaSource);
    }

    @Override
    public void prepare(MediaSource mediaSource, boolean resetPosition, boolean resetState) {
        player.prepare(mediaSource, resetPosition, resetState);
    }

    @Override
    public void setPlayWhenReady(boolean playWhenReady) {
        player.setPlayWhenReady(playWhenReady);
    }

    @Override
    public boolean getPlayWhenReady() {
        return player.getPlayWhenReady();
    }

    @Override
    public @RepeatMode
    int getRepeatMode() {
        return player.getRepeatMode();
    }

    @Override
    public void setRepeatMode(@RepeatMode int repeatMode) {
        player.setRepeatMode(repeatMode);
    }

    @Override
    public void setShuffleModeEnabled(boolean shuffleModeEnabled) {
        player.setShuffleModeEnabled(shuffleModeEnabled);
    }

    @Override
    public boolean getShuffleModeEnabled() {
        return player.getShuffleModeEnabled();
    }

    @Override
    public boolean isLoading() {
        return player.isLoading();
    }

    @Override
    public void seekToDefaultPosition() {
        player.seekToDefaultPosition();
    }

    @Override
    public void seekToDefaultPosition(int windowIndex) {
        player.seekToDefaultPosition(windowIndex);
    }

    @Override
    public void seekTo(long positionMs) {
        player.seekTo(positionMs);
    }

    @Override
    public void seekTo(int windowIndex, long positionMs) {
        player.seekTo(windowIndex, positionMs);
    }

    @Override
    public void setPlaybackParameters(@Nullable PlaybackParameters playbackParameters) {
        player.setPlaybackParameters(playbackParameters);
    }

    @Override
    public PlaybackParameters getPlaybackParameters() {
        return player.getPlaybackParameters();
    }

    @Override
    public void setSeekParameters(@Nullable SeekParameters seekParameters) {
        player.setSeekParameters(seekParameters);
    }

    @Override
    public void stop() {
        player.stop();
    }

    @Override
    public void stop(boolean reset) {
        player.stop(reset);
    }

    @Override
    public void release() {
        player.release();
        removeSurfaceCallbacks();
        if (surface != null) {
            if (ownsSurface) {
                surface.release();
            }
            surface = null;
        }
    }

    @Override
    public void sendMessages(ExoPlayerMessage... messages) {
        player.sendMessages(messages);
    }

    @Override
    public PlayerMessage createMessage(PlayerMessage.Target target) {
        return player.createMessage(target);
    }

    @Override
    public void blockingSendMessages(ExoPlayerMessage... messages) {
        player.blockingSendMessages(messages);
    }

    @Override
    public int getRendererCount() {
        return player.getRendererCount();
    }

    @Override
    public int getRendererType(int index) {
        return player.getRendererType(index);
    }

    @Override
    public TrackGroupArray getCurrentTrackGroups() {
        return player.getCurrentTrackGroups();
    }

    @Override
    public TrackSelectionArray getCurrentTrackSelections() {
        return player.getCurrentTrackSelections();
    }

    @Override
    public Timeline getCurrentTimeline() {
        return player.getCurrentTimeline();
    }

    @Override
    public Object getCurrentManifest() {
        return player.getCurrentManifest();
    }

    @Override
    public int getCurrentPeriodIndex() {
        return player.getCurrentPeriodIndex();
    }

    @Override
    public int getCurrentWindowIndex() {
        return player.getCurrentWindowIndex();
    }

    @Override
    public int getNextWindowIndex() {
        return player.getNextWindowIndex();
    }

    @Override
    public int getPreviousWindowIndex() {
        return player.getPreviousWindowIndex();
    }

    @Override
    public long getDuration() {
        return player.getDuration();
    }

    @Override
    public long getCurrentPosition() {
        return player.getCurrentPosition();
    }

    @Override
    public long getBufferedPosition() {
        return player.getBufferedPosition();
    }

    @Override
    public int getBufferedPercentage() {
        return player.getBufferedPercentage();
    }

    @Override
    public boolean isCurrentWindowDynamic() {
        return player.isCurrentWindowDynamic();
    }

    @Override
    public boolean isCurrentWindowSeekable() {
        return player.isCurrentWindowSeekable();
    }

    @Override
    public boolean isPlayingAd() {
        return player.isPlayingAd();
    }

    @Override
    public int getCurrentAdGroupIndex() {
        return player.getCurrentAdGroupIndex();
    }

    @Override
    public int getCurrentAdIndexInAdGroup() {
        return player.getCurrentAdIndexInAdGroup();
    }

    @Override
    public long getContentPosition() {
        return player.getContentPosition();
    }

    // Internal methods.

    /**
     * Creates the {@link ExoPlayer} implementation used by this instance.
     *
     * @param renderers     The {@link Renderer}s that will be used by the instance.
     * @param trackSelector The {@link TrackSelector} that will be used by the instance.
     * @param loadControl   The {@link LoadControl} that will be used by the instance.
     * @param clock         The {@link Clock} that will be used by this instance.
     * @return A new {@link ExoPlayer} instance.
     */
    protected ExoPlayer createExoPlayerImpl(Renderer[] renderers, TrackSelector trackSelector, LoadControl loadControl, Clock clock) {
        return new ExoPlayerImpl(renderers, trackSelector, loadControl, clock);
    }

    private void removeSurfaceCallbacks() {
        if (textureView != null) {
            if (textureView.getSurfaceTextureListener() != componentListener) {
                Log.w(TAG, "SurfaceTextureListener already unset or replaced.");
            } else {
                textureView.setSurfaceTextureListener(null);
            }
            textureView = null;
        }
        if (surfaceHolder != null) {
            surfaceHolder.removeCallback(componentListener);
            surfaceHolder = null;
        }
    }

    private void setVideoSurfaceInternal(Surface surface, boolean ownsSurface) {
        // Note: We don't turn this method into a no-op if the surface is being replaced with itself
        // so as to ensure onRenderedFirstFrame callbacks are still called in this case.
        List<PlayerMessage> messages = new ArrayList<>();
        for (Renderer renderer : renderers) {
            if (renderer.getTrackType() == C.TRACK_TYPE_VIDEO) {
                messages.add(player.createMessage(renderer).setType(C.MSG_SET_SURFACE).setPayload(surface).send());
            }
        }
        if (this.surface != null && this.surface != surface) {
            // We're replacing a surface. Block to ensure that it's not accessed after the method returns.
            try {
                for (PlayerMessage message : messages) {
                    message.blockUntilDelivered();
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
            // If we created the previous surface, we are responsible for releasing it.
            if (this.ownsSurface) {
                this.surface.release();
            }
        }
        this.surface = surface;
        this.ownsSurface = ownsSurface;
    }

    private final class ComponentListener implements VideoRendererEventListener, AudioRendererEventListener, TextOutput, MetadataOutput, SurfaceHolder.Callback, TextureView.SurfaceTextureListener {

        // VideoRendererEventListener implementation

        @Override
        public void onVideoEnabled(DecoderCounters counters) {
            videoDecoderCounters = counters;
            for (VideoRendererEventListener videoDebugListener : videoDebugListeners) {
                videoDebugListener.onVideoEnabled(counters);
            }
        }

        @Override
        public void onVideoDecoderInitialized(String decoderName, long initializedTimestampMs, long initializationDurationMs) {
            for (VideoRendererEventListener videoDebugListener : videoDebugListeners) {
                videoDebugListener.onVideoDecoderInitialized(decoderName, initializedTimestampMs, initializationDurationMs);
            }
        }

        @Override
        public void onVideoInputFormatChanged(Format format) {
            videoFormat = format;
            for (VideoRendererEventListener videoDebugListener : videoDebugListeners) {
                videoDebugListener.onVideoInputFormatChanged(format);
            }
        }

        @Override
        public void onDroppedFrames(int count, long elapsed) {
            for (VideoRendererEventListener videoDebugListener : videoDebugListeners) {
                videoDebugListener.onDroppedFrames(count, elapsed);
            }
        }

        @Override
        public void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees, float pixelWidthHeightRatio) {
            for (com.google.android.exoplayer2.video.VideoListener videoListener : videoListeners) {
                videoListener.onVideoSizeChanged(width, height, unappliedRotationDegrees, pixelWidthHeightRatio);
            }
            for (VideoRendererEventListener videoDebugListener : videoDebugListeners) {
                videoDebugListener.onVideoSizeChanged(width, height, unappliedRotationDegrees, pixelWidthHeightRatio);
            }
        }

        @Override
        public void onRenderedFirstFrame(Surface surface) {
            if (SimpleExoPlayer.this.surface == surface) {
                for (com.google.android.exoplayer2.video.VideoListener videoListener : videoListeners) {
                    videoListener.onRenderedFirstFrame();
                }
            }
            for (VideoRendererEventListener videoDebugListener : videoDebugListeners) {
                videoDebugListener.onRenderedFirstFrame(surface);
            }
        }

        @Override
        public void onVideoDisabled(DecoderCounters counters) {
            for (VideoRendererEventListener videoDebugListener : videoDebugListeners) {
                videoDebugListener.onVideoDisabled(counters);
            }
            videoFormat = null;
            videoDecoderCounters = null;
        }

        // AudioRendererEventListener implementation

        @Override
        public void onAudioEnabled(DecoderCounters counters) {
            audioDecoderCounters = counters;
            for (AudioRendererEventListener audioDebugListener : audioDebugListeners) {
                audioDebugListener.onAudioEnabled(counters);
            }
        }

        @Override
        public void onAudioSessionId(int sessionId) {
            audioSessionId = sessionId;
            for (AudioRendererEventListener audioDebugListener : audioDebugListeners) {
                audioDebugListener.onAudioSessionId(sessionId);
            }
        }

        @Override
        public void onAudioDecoderInitialized(String decoderName, long initializedTimestampMs, long initializationDurationMs) {
            for (AudioRendererEventListener audioDebugListener : audioDebugListeners) {
                audioDebugListener.onAudioDecoderInitialized(decoderName, initializedTimestampMs, initializationDurationMs);
            }
        }

        @Override
        public void onAudioInputFormatChanged(Format format) {
            audioFormat = format;
            for (AudioRendererEventListener audioDebugListener : audioDebugListeners) {
                audioDebugListener.onAudioInputFormatChanged(format);
            }
        }

        @Override
        public void onAudioSinkUnderrun(int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) {
            for (AudioRendererEventListener audioDebugListener : audioDebugListeners) {
                audioDebugListener.onAudioSinkUnderrun(bufferSize, bufferSizeMs, elapsedSinceLastFeedMs);
            }
        }

        @Override
        public void onAudioDisabled(DecoderCounters counters) {
            for (AudioRendererEventListener audioDebugListener : audioDebugListeners) {
                audioDebugListener.onAudioDisabled(counters);
            }
            audioFormat = null;
            audioDecoderCounters = null;
            audioSessionId = C.AUDIO_SESSION_ID_UNSET;
        }

        // TextOutput implementation

        @Override
        public void onCues(List<Cue> cues) {
            for (TextOutput textOutput : textOutputs) {
                textOutput.onCues(cues);
            }
        }

        // MetadataOutput implementation

        @Override
        public void onMetadata(Metadata metadata) {
            for (MetadataOutput metadataOutput : metadataOutputs) {
                metadataOutput.onMetadata(metadata);
            }
        }

        // SurfaceHolder.Callback implementation

        @Override
        public void surfaceCreated(SurfaceHolder holder) {
            setVideoSurfaceInternal(holder.getSurface(), false);
        }

        @Override
        public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
            // Do nothing.
        }

        @Override
        public void surfaceDestroyed(SurfaceHolder holder) {
            setVideoSurfaceInternal(null, false);
        }

        // TextureView.SurfaceTextureListener implementation

        @Override
        public void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture, int width, int height) {
            setVideoSurfaceInternal(new Surface(surfaceTexture), true);
        }

        @Override
        public void onSurfaceTextureSizeChanged(SurfaceTexture surfaceTexture, int width, int height) {
            // Do nothing.
        }

        @Override
        public boolean onSurfaceTextureDestroyed(SurfaceTexture surfaceTexture) {
            setVideoSurfaceInternal(null, true);
            return true;
        }

        @Override
        public void onSurfaceTextureUpdated(SurfaceTexture surfaceTexture) {
            // Do nothing.
        }

    }

}
